import json
import os
import threading
import time
import tkinter as tk
import webbrowser
from tkinter import filedialog, messagebox, ttk
from urllib.parse import urlparse

import requests


# ====================== 核心功能模块 ======================
def fetch_swagger(swagger_url):
    """获取Swagger JSON数据"""
    try:
        response = requests.get(swagger_url, timeout=10, verify=False)
        response.raise_for_status()
        return response.json()
    except Exception as e:
        raise Exception(f"获取Swagger数据失败: {str(e)}")


def generate_config(swagger_data, output_dir, swagger_url):
    """从Swagger数据生成配置文件"""
    try:
        endpoints = []
        # host = swagger_data.get('host', 'localhost:8080')
        # 解析用户输入的Swagger URL获取基础前缀
        parsed_swagger_url = urlparse(swagger_url)
        base_prefix = f"{parsed_swagger_url.scheme}://{parsed_swagger_url.netloc}"

        # 获取Swagger中的基础路径（如果有）
        base_path = swagger_data.get('basePath', '/')

        # 处理URL格式
        if not base_path.startswith('/'):
            base_path = '/' + base_path
        if not base_path.endswith('/'):
            base_path += '/'

        # 处理协议
        scheme = "http"
        if "schemes" in swagger_data and swagger_data["schemes"]:
            scheme = swagger_data["schemes"][0]

        # 提取所有路径
        for path, methods in swagger_data['paths'].items():
            if not path.startswith('/'):
                path = '/' + path

            for method, details in methods.items():
                if method.lower() in ['get', 'post', 'put', 'delete', 'patch']:
                    # 使用Swagger URL前缀 + basePath + 接口路径
                    full_url = f"{base_prefix}{base_path}{path}"
                    # full_url = f"{scheme}://{host}{base_path}{path.lstrip('/')}"

                    endpoint = {
                        "url": full_url,
                        "method": method.upper()
                    }

                    if method.lower() in ['post', 'put']:
                        consumes = details.get('consumes', [])
                        if consumes and 'application/json' in consumes:
                            endpoint["headers"] = {"Content-Type": "application/json"}

                    endpoints.append(endpoint)

        config = {"endpoints": endpoints}
        config_path = os.path.join(output_dir, "config.json")

        with open(config_path, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)

        return config_path, len(endpoints)
    except Exception as e:
        raise Exception(f"生成配置文件失败: {str(e)}")


def decode_unicode_escape(text):
    """将Unicode转义序列转换为中文字符"""
    try:
        return text.encode('utf-8').decode('unicode_escape')
    except:
        return text


def scan_endpoints(config_path, output_path, safe_codes):
    """扫描所有接口的未授权访问"""
    try:
        # 加载配置
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
            endpoints = config.get('endpoints', [])

        results = {
            'vulnerable': [],
            'secured': [],
            'errors': [],
            'summary': {
                'total': len(endpoints),
                'vulnerable': 0,
                'secured': 0,
                'errors': 0
            }
        }

        # 扫描每个端点
        for endpoint in endpoints:
            try:
                url = endpoint['url']
                method = endpoint.get('method', 'GET').upper()
                headers = endpoint.get('headers', {})
                params = endpoint.get('params', {})
                data = endpoint.get('data', {})

                # 移除认证头
                headers.pop('Authorization', None)
                headers.pop('authorization', None)

                # 发送请求
                start_time = time.time()
                response = requests.request(
                    method=method,
                    url=url,
                    headers=headers,
                    params=params,
                    json=data if headers.get('Content-Type') == 'application/json' else None,
                    data=data if headers.get('Content-Type') != 'application/json' else None,
                    timeout=10,
                    verify=False
                )
                elapsed = round((time.time() - start_time) * 1000, 2)

                # 分析结果
                status = f"{response.status_code} {response.reason}"

                # 处理中文响应
                try:
                    response_text = response.text
                except UnicodeDecodeError:
                    response_text = response.content.decode('utf-8', errors='replace')

                print("status:" + status)
                print("response_text:" + response_text)

                # 解码Unicode转义序列
                decoded_text = decode_unicode_escape(response_text)

                # 判断是否为安全接口
                is_secure = False


                # 1. 检查自定义安全码
                if safe_codes:
                    for code in safe_codes:
                        if code in response_text:
                            is_secure = True
                            break

                # 2. 检查状态码
                if not is_secure and response.status_code == 200 and 'error' not in response.text.lower():
                    is_secure = False

                # 记录结果
                if is_secure:
                    results['secured'].append({
                        'url': url,
                        'method': method,
                        'status': status,
                        'time_ms': elapsed,
                        'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text
                    })
                    results['summary']['secured'] += 1
                else:
                    result = {
                        'url': url,
                        'method': method,
                        'status': status,
                        'time_ms': elapsed,
                        'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text
                    }
                    results['vulnerable'].append(result)
                    results['summary']['vulnerable'] += 1

            except Exception as e:
                error = f"请求失败: {method} {url} - {str(e)}"
                results['errors'].append(error)
                results['summary']['errors'] += 1

        # 保存结果
        result_path = os.path.join(output_path, "scan_results.json")
        with open(result_path, 'w', encoding='utf-8') as f:
            json.dump(results, f, indent=2, ensure_ascii=False)

        return result_path, results['summary']
    except Exception as e:
        raise Exception(f"扫描失败: {str(e)}")


# ====================== GUI界面模块 ======================
class APIScannerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("API未授权访问扫描工具")
        self.root.geometry("600x450")
        self.root.resizable(True, True)

        # 设置样式
        self.style = ttk.Style()
        self.style.configure("TLabel", padding=5, font=("Arial", 10))
        self.style.configure("TButton", padding=5, font=("Arial", 10))
        self.style.configure("TEntry", padding=5, font=("Arial", 10))
        self.style.configure("Header.TLabel", font=("Arial", 12, "bold"))

        # 创建主框架
        main_frame = ttk.Frame(root, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 标题
        ttk.Label(main_frame, text="API未授权访问扫描工具", style="Header.TLabel").grid(row=0, column=0, columnspan=3,
                                                                                        pady=10)

        # Swagger地址
        ttk.Label(main_frame, text="Swagger地址:").grid(row=1, column=0, sticky=tk.W, pady=5)
        self.swagger_url = tk.StringVar(value="http://127.0.0.1:8080/v2/api-docs")
        swagger_entry = ttk.Entry(main_frame, textvariable=self.swagger_url, width=50)
        swagger_entry.grid(row=1, column=1, sticky=tk.W + tk.E, padx=5)

        # 输出目录
        ttk.Label(main_frame, text="输出目录:").grid(row=2, column=0, sticky=tk.W, pady=5)
        self.output_dir = tk.StringVar()
        output_entry = ttk.Entry(main_frame, textvariable=self.output_dir, width=50)
        output_entry.grid(row=2, column=1, sticky=tk.W + tk.E, padx=5)
        ttk.Button(main_frame, text="浏览...", command=self.browse_output_dir).grid(row=2, column=2, padx=5)

        # 自定义安全码
        ttk.Label(main_frame, text="自定义安全码(非必填):").grid(row=3, column=0, sticky=tk.W, pady=5)
        self.safe_codes = tk.StringVar(value="404")
        safe_entry = ttk.Entry(main_frame, textvariable=self.safe_codes, width=50)
        safe_entry.grid(row=3, column=1, sticky=tk.W + tk.E, padx=5)
        ttk.Label(main_frame, text="例如: 404,3006", foreground="gray").grid(row=3, column=2, sticky=tk.W)

        # 进度条
        self.progress = ttk.Progressbar(main_frame, orient=tk.HORIZONTAL, length=500, mode='determinate')
        self.progress.grid(row=4, column=0, columnspan=3, pady=20, sticky=tk.W + tk.E)

        # 状态标签
        self.status = tk.StringVar(value="就绪")
        status_label = ttk.Label(main_frame, textvariable=self.status)
        status_label.grid(row=5, column=0, columnspan=3, pady=5)

        # 操作按钮
        btn_frame = ttk.Frame(main_frame)
        btn_frame.grid(row=6, column=0, columnspan=3, pady=10)

        self.scan_btn = ttk.Button(btn_frame, text="开始扫描", command=self.start_scan)
        self.scan_btn.pack(side=tk.LEFT, padx=10)

        ttk.Button(btn_frame, text="打开结果目录", command=self.open_output_dir).pack(side=tk.LEFT, padx=10)
        ttk.Button(btn_frame, text="退出", command=root.quit).pack(side=tk.LEFT, padx=10)

        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="扫描日志")
        log_frame.grid(row=7, column=0, columnspan=3, sticky=tk.W + tk.E + tk.N + tk.S, pady=10)

        self.log_text = tk.Text(log_frame, height=8, width=70)
        self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.log_text.config(state=tk.DISABLED)

        # 配置网格权重
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(7, weight=1)

    def browse_output_dir(self):
        """选择输出目录"""
        directory = filedialog.askdirectory()
        if directory:
            self.output_dir.set(directory)

    def open_output_dir(self):
        """打开输出目录"""
        if self.output_dir.get():
            try:
                webbrowser.open(self.output_dir.get())
            except:
                messagebox.showerror("错误", "无法打开目录")

    def log_message(self, message):
        """记录日志消息"""
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)

    def start_scan(self):
        """开始扫描过程"""
        # 验证输入
        swagger_url = self.swagger_url.get().strip()
        output_dir = self.output_dir.get().strip()
        safe_codes = [code.strip() for code in self.safe_codes.get().split(",") if code.strip()]

        if not swagger_url:
            messagebox.showerror("错误", "请输入Swagger地址")
            return

        if not output_dir:
            messagebox.showerror("错误", "请选择输出目录")
            return

        # 创建输出目录
        try:
            os.makedirs(output_dir, exist_ok=True)
        except Exception as e:
            messagebox.showerror("错误", f"无法创建输出目录: {str(e)}")
            return

        # 禁用按钮
        self.scan_btn.config(state=tk.DISABLED)
        self.status.set("开始扫描...")
        self.progress["value"] = 0
        self.log_message("=" * 50)
        self.log_message("开始扫描过程")
        self.log_message(f"Swagger地址: {swagger_url}")
        self.log_message(f"输出目录: {output_dir}")
        if safe_codes:
            self.log_message(f"自定义安全码: {', '.join(safe_codes)}")
        else:
            self.log_message("未设置自定义安全码")

        # 在后台线程中运行扫描
        threading.Thread(target=self.run_scan, args=(swagger_url, output_dir, safe_codes), daemon=True).start()

    def run_scan(self, swagger_url, output_dir, safe_codes):
        """执行扫描过程"""
        try:
            # 步骤1: 获取Swagger数据
            self.status.set("获取Swagger数据...")
            self.log_message("\n步骤1: 获取Swagger数据")
            swagger_data = fetch_swagger(swagger_url)
            self.log_message("✓ Swagger数据获取成功")
            self.progress["value"] = 20

            # 步骤2: 生成配置文件
            self.status.set("生成配置文件...")
            self.log_message("\n步骤2: 生成配置文件")
            config_path, endpoint_count = generate_config(swagger_data, output_dir, swagger_url)
            self.log_message(f"✓ 配置文件生成成功: {config_path}")
            self.log_message(f"共发现 {endpoint_count} 个API端点")
            self.progress["value"] = 40

            # 步骤3: 扫描端点
            self.status.set("扫描API端点...")
            self.log_message("\n步骤3: 扫描API端点")
            self.log_message(f"开始扫描 {endpoint_count} 个端点...")
            result_path, summary = scan_endpoints(config_path, output_dir, safe_codes)
            self.log_message(f"✓ 扫描完成! 结果保存到: {result_path}")
            self.progress["value"] = 80

            # 显示摘要
            self.log_message("\n扫描摘要:")
            self.log_message(f"总端点: {summary['total']}")
            self.log_message(f"漏洞端点: {summary['vulnerable']}")
            self.log_message(f"安全端点: {summary['secured']}")
            self.log_message(f"错误: {summary['errors']}")

            self.progress["value"] = 100
            self.status.set(f"扫描完成! 发现 {summary['vulnerable']} 个漏洞")
            messagebox.showinfo("完成", f"扫描完成! 发现 {summary['vulnerable']} 个未授权访问漏洞")

        except Exception as e:
            self.log_message(f"\n❌ 错误: {str(e)}")
            self.status.set("扫描失败")
            messagebox.showerror("错误", str(e))

        finally:
            self.scan_btn.config(state=tk.NORMAL)


# ====================== 主程序入口 ======================
if __name__ == "__main__":
    root = tk.Tk()
    app = APIScannerApp(root)
    root.mainloop()
